useObservable

Scripting 提供一套响应式状态系统,由 Observable<T>useObservable<T> 组成,用于驱动组件渲染、与动画系统协同工作,并与 SwiftUI 的双向绑定能力保持一致(例如 List(selection:)NavigationStack(path:) 等未来扩展接口)。


1. Observable<T>

Observable<T> 是一个可观察的数据容器,当 .value 更新时,会触发依赖该值的 UI 自动重新渲染。

1.1 类定义

1class Observable<T> {
2  constructor(initialValue: T);
3  value: T;
4  setValue(value: T): void;
5  subscribe(callback: (value: T, oldValue: T) => void): void;
6  unsubscribe(callback: (value: T, oldValue: T) => void): void;
7  dispose(): void;
8}

1.2 属性与方法说明

value

存储当前值。读取 .value 不会产生副作用。

setValue(newValue)

更新值,并触发 UI 重绘:

1observable.setValue(newValue);

支持任何类型 T(包括对象、数组、字面量、类实例等)。

subscribe / unsubscribe

用于在组件体系外手动监听值变化。

dispose

释放监听器和内部资源。 一般无需手动调用,仅在高级场景使用。


2. useObservable<T>

useObservable<T> 是在组件内部创建本地状态的 Hook。 返回值为 Observable<T>,用于驱动 UI 更新。

2.1 函数签名

1declare function useObservable<T>(): Observable<T | undefined>;
2declare function useObservable<T>(value: T): Observable<T>;
3declare function useObservable<T>(initializer: () => T): Observable<T>;

2.2 初始化方式

1. 无初始值(value 为 undefined)

1const data = useObservable<string>();

2. 直接提供初始值

1const count = useObservable(0);

3. 惰性初始化(初次渲染时执行)

1const user = useObservable(() => createDefaultUser());

3. 在 UI 中使用 Observable

在组件中,只需读取 .value

1<Text>{name.value}</Text>

.setValue 被调用,组件会自动重新渲染:

1<Button title="Tap" action={() => name.setValue("Updated")} />

无需手动触发更新,行为与 React useState 类似,但带来更 SwiftUI 式的数据绑定体验。


4. 与动画协同工作

Observable 是动画触发源。 支持以下场景:

4.1 显式动画:withAnimation

1withAnimation(() => {
2  size.setValue(size.value + 20);
3});

任何依赖 size.value 的视图都会执行动画。


4.2 隐式动画:animation 修饰符

视图可通过 animation 属性监听某个值的变化并执行动画。

正确写法:

1animation={{
2  animation: Animation.spring({ duration: 0.3 }),
3  value: size.value
4}}

示例:

1<Rectangle
2  frame={{
3    width: size.value,
4    height: size.value,
5  }}
6  animation={{
7    animation: Animation.easeIn(0.25),
8    value: size.value,
9  }}
10/>

5. 与 SwiftUI Binding 风格的 API 对接(扩展能力)

Observable 将作为未来 Scripting 的标准双向绑定机制,用于支持 SwiftUI 风格的 API,例如:

5.1 List(selection:)

1const selection = useObservable<string | undefined>(undefined)
2
3<List selection={selection}>
4  ...
5</List>

5.2 NavigationStack(path:)

1const path = useObservable<string[]>([])
2
3<NavigationStack path={path}>
4  ...
5</NavigationStack>

这类 API 使用方式与 SwiftUI 一致,开发者无需学习额外的绑定机制。


6. ForEach:推荐使用 Observable 数据源

为了获得更接近 SwiftUI 的体验,推荐使用:

1<ForEach data={observableArray} builder={(item, index) => <Text>{item.name}</Text>} />

其中:

1T extends { id: string }

为什么推荐这种写法:

  • 性能更佳
  • 插入与删除动画体验更自然

示例:

1const items = useObservable([
2  { id: "1", name: "Apple" },
3  { id: "2", name: "Banana" }
4])
5
6<ForEach
7  data={items}
8  editActions="all"
9  builder={(item) => <Text>{item.name}</Text>}
10/>

7. 综合示例

1export function Demo() {
2  const visible = useObservable(true);
3  const size = useObservable(100);
4
5  return (
6    <VStack spacing={20}>
7      {visible.value && (
8        <Rectangle
9          frame={{
10            width: size.value,
11            height: size.value,
12          }}
13          background="blue"
14          animation={{
15            animation: Animation.spring({ duration: 0.4, bounce: 0.3 }),
16            value: size.value,
17          }}
18          transition={Transition.opacity()}
19        />
20      )}
21
22      <Button
23        title="Toggle Visible"
24        action={() => {
25          withAnimation(() => {
26            visible.setValue(!visible.value);
27          });
28        }}
29      />
30
31      <Button
32        title="Resize"
33        action={() => {
34          withAnimation(Animation.easeOut(0.25), () => {
35            size.setValue(size.value === 100 ? 160 : 100);
36          });
37        }}
38      />
39    </VStack>
40  );
41}

8. 总结

  • Observable<T> 是 Scripting 中的核心响应式数据结构
  • useObservable 在组件内创建状态,支持任意类型 T
  • 与 UI 自动联动,无需额外刷新逻辑
  • 为动画系统提供依赖值,用于属性动画与显式动画
  • 为未来的 SwiftUI 风格 API 提供双向绑定能力
  • ForEach 推荐使用 data: Observable<Array<T>>,获得一致的 SwiftUI 体验